package com.indi.gulimall.seckill.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.indi.common.constant.OrderConstant;
import com.indi.common.constant.SeckillConstant;
import com.indi.common.dto.SeckillSessionDTO;
import com.indi.common.dto.SeckillSkuRedisDTO;
import com.indi.common.dto.SkuInfoDTO;
import com.indi.common.dto.mq.SeckillSkuDTO;
import com.indi.common.enums.BizCodeEnum;
import com.indi.common.exception.Assert;
import com.indi.common.to.MemberTO;
import com.indi.common.utils.R;
import com.indi.gulimall.seckill.feign.CouponFeignService;
import com.indi.gulimall.seckill.feign.ProductFeignService;
import com.indi.gulimall.seckill.interceptor.LoginUserInterceptor;
import com.indi.gulimall.seckill.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author UnityAlvin
 * @date 2021/7/27 9:12
 */
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {
    @Resource
    private CouponFeignService couponFeignService;

    @Resource
    private ProductFeignService productFeignService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Resource
    private ThreadPoolExecutor executor;

    @Override
    public void uploadSeckillSkuLatest3Days() {
        R r = couponFeignService.listByLatest3Days();
        Assert.isTrue(r.getCode() == 0, BizCodeEnum.FEIGN_SERVICE_EXCEPTION);
        List<SeckillSessionDTO> seckillSessionDTOs = r.getData(new TypeReference<List<SeckillSessionDTO>>() {
        });

        if (CollectionUtils.isNotEmpty(seckillSessionDTOs)) {
            // 缓存秒杀活动场次
            cacheSeckillSession(seckillSessionDTOs);

            // 缓存秒杀商品
            cacheSeckillSkus(seckillSessionDTOs);
        }
    }

    @Override
    public List<SeckillSkuRedisDTO> getCurrentSeckillSkus() {
        long currentTime = new Date().getTime();
        // 获取redis中所有指定格式的key
        Set<String> keys = stringRedisTemplate.keys(SeckillConstant.SESSIONS_CACHE_PREFIX + "*");
        // 当前key的值：gulimall:seckill:sessions:1627437600000_1627439400000
        for (String key : keys) {
            String newKey = key.replace(SeckillConstant.SESSIONS_CACHE_PREFIX, "");
            String[] s = newKey.split("_");
            long startTime = Long.parseLong(s[0]);
            long endTime = Long.parseLong(s[1]);
            // 遍历所有秒杀场次，确定当前时间属于哪个场次
            if (currentTime >= startTime && currentTime <= endTime) {
                // 从redis的list中取出索引从-100到100的key所对应的值
                // [1_1,2_2,...]
                List<String> skuIds = stringRedisTemplate.opsForList().range(key, -100, 100);

                BoundHashOperations<String, String, String> skusOperations = stringRedisTemplate
                        .boundHashOps(SeckillConstant.SECKILL_SKU_PREFIX);
                // 从Hash中批量获取多个key的值
                // [json格式的SeckillSkuRedisDTO,json格式的SeckillSkuRedisDTO,...]
                List<String> values = skusOperations.multiGet(skuIds);
                if (CollectionUtils.isNotEmpty(values)) {
                    List<SeckillSkuRedisDTO> seckillSkuRedisDTOs = values.stream().map(value -> {
                        SeckillSkuRedisDTO seckillSkuRedisDTO = JSON.parseObject(value, SeckillSkuRedisDTO.class);
                        return seckillSkuRedisDTO;
                    }).collect(Collectors.toList());
                    return seckillSkuRedisDTOs;
                }
            }
        }
        return null;
    }

    @Override
    public SeckillSkuRedisDTO getSeckillSkuRelation(Long skuId) {
        // 获取redis中存储sku的hash
        BoundHashOperations<String, String, String> skusOperations = stringRedisTemplate.boundHashOps(SeckillConstant
                .SECKILL_SKU_PREFIX);
        Set<String> keys = skusOperations.keys();
        if (CollectionUtils.isNotEmpty(keys)) {
            String regex = "\\d_" + skuId;  // 正则：1位数字_skuId
            for (String key : keys) {
                // 匹配所有key
                if (Pattern.matches(regex, key)) {
                    // 从hash中获取到指定的值
                    String jsonStr = skusOperations.get(key);
                    SeckillSkuRedisDTO seckillSkuRedisDTO = JSON.parseObject(jsonStr, SeckillSkuRedisDTO.class);

                    long now = new Date().getTime();
                    Long startTime = seckillSkuRedisDTO.getStartTime();
                    Long endTime = seckillSkuRedisDTO.getEndTime();
                    if (now < startTime || now > endTime) {
                        // 如果当前不是秒杀时间，则不应该返回随机码
                        seckillSkuRedisDTO.setRandomCode(null);
                    }
                    return seckillSkuRedisDTO;
                }
            }
        }
        return null;
    }

    @Override
    public SeckillSkuDTO kill(String killId, String randomCode, Integer num) {
        long s1 = System.currentTimeMillis();
        MemberTO memberTO = LoginUserInterceptor.threadLocal.get();
        // 获取当前killid的sku信息
        BoundHashOperations<String, String, String> skusOperations = stringRedisTemplate
                .boundHashOps(SeckillConstant.SECKILL_SKU_PREFIX);
        String jsonStr = skusOperations.get(killId);
        if (StringUtils.isNotEmpty(jsonStr)) {
            SeckillSkuRedisDTO seckillSkuRedisDTO = JSON.parseObject(jsonStr, SeckillSkuRedisDTO.class);

            // 校验时间
            long now = new Date().getTime();
            Long startTime = seckillSkuRedisDTO.getStartTime();
            Long endTime = seckillSkuRedisDTO.getEndTime();
            long ttl = endTime - now;

            if (now >= startTime || now <= endTime) {
                // 校验随机码
                if (seckillSkuRedisDTO.getRandomCode().equals(randomCode)) {
                    // 验证购买数量是否合理
                    if (num <= seckillSkuRedisDTO.getSeckillSkuRelationDTO().getSeckillLimit()) {
                        // 校验用户是否已经购买过，去redis占位absent，需要自动过期，
                        // userId_promotionSessionId_skuId，end-now，milliseconds
                        String key = SeckillConstant.SECKILL_ABSENT_PREFIX + memberTO.getId() + "_" + killId;
                        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, num.toString(), ttl,
                                TimeUnit.MILLISECONDS);
                        if (absent) {
                            // 占位成功，说明没买过，减信号量，tryAcquire (num,100,seconds)
                            RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant
                                    .SECKILL_SKU_STOCK_SEMAPHORE + randomCode);
                            boolean acquire = semaphore.tryAcquire(num);
                            if (acquire) {
                                // 秒杀成功，返回数据
                                SeckillSkuDTO seckillSkuDTO = new SeckillSkuDTO();
                                String[] s = killId.split("_");
                                seckillSkuDTO.setOrderSn(IdWorker.getTimeId());
                                seckillSkuDTO.setMemberId(memberTO.getId());
                                seckillSkuDTO.setPromotionSessionId(Long.parseLong(s[0]));
                                seckillSkuDTO.setSkuId(Long.parseLong(s[1]));

                                BigDecimal seckillPrice = seckillSkuRedisDTO.getSeckillSkuRelationDTO()
                                        .getSeckillPrice();
                                seckillSkuDTO.setPrice(seckillPrice);
                                seckillSkuDTO.setNum(num);
                                seckillSkuDTO.setTitle(seckillSkuRedisDTO.getSkuInfoDTO().getSkuTitle());
                                seckillSkuDTO.setImage(seckillSkuRedisDTO.getSkuInfoDTO().getSkuDefaultImg());
                                seckillSkuDTO.setSkuAttr(seckillSkuRedisDTO.getSkuAttr());
                                seckillSkuDTO.setRandomCode(randomCode);

                                rabbitTemplate.convertAndSend(OrderConstant.ORDER_EVENT_EXCHANGE, OrderConstant.ORDER_SECKILL_ORDER_ROUTING_KEY,
                                        seckillSkuDTO);
                                long s2 = System.currentTimeMillis();
                                log.info("花费时间：{0}", s2 - s1);
                                return seckillSkuDTO;
                            } else {
                                // 秒杀失败，则删除占位，
                                // 只有秒杀成功，占位才会保存下来，占位以后可以作为用户秒杀成功的依据
                                // 秒杀失败，占位则不会保存，这就意味着这个用户从来没有抢购过，抢购失败=没有抢购过
                                stringRedisTemplate.delete(key);
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    @Override
    public void releaseStock(SeckillSkuDTO seckillSkuDTO) {
        // 先获取到当前用户秒杀的数量
        String key = SeckillConstant.SECKILL_ABSENT_PREFIX + seckillSkuDTO.getMemberId() + "_" +
                seckillSkuDTO.getPromotionSessionId() + "_" + seckillSkuDTO.getSkuId();
        String value = stringRedisTemplate.opsForValue().get(key);
        Integer num = Integer.parseInt(value);
        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, num.toString());
        if (!absent) {
            stringRedisTemplate.delete(key);
        }

        // 删除占的锁
        stringRedisTemplate.delete(key);

        // 获取到信号量，增加
        RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant
                .SECKILL_SKU_STOCK_SEMAPHORE + seckillSkuDTO.getRandomCode());
        semaphore.release(num);
    }

    /**
     * 缓存秒杀商品
     *
     * @param seckillSessionDTOs
     */
    private void cacheSeckillSkus(List<SeckillSessionDTO> seckillSessionDTOs) {
        seckillSessionDTOs.stream().forEach(item -> {
            // 绑定Redis的Hash，准备Hash操作
            BoundHashOperations<String, Object, Object> skusOperations = stringRedisTemplate
                    .boundHashOps(SeckillConstant.SECKILL_SKU_PREFIX);

            item.getRelations().stream().forEach(relation -> {

                String skuRedisKey = relation.getPromotionSessionId() + "_" + relation.getSkuId();
                // 幂等性保证
                if (!skusOperations.hasKey(skuRedisKey)) {
                    SeckillSkuRedisDTO seckillSkuRedisDTO = new SeckillSkuRedisDTO();

                    // sku基本数据
                    CompletableFuture<Void> skuInfoFuture = CompletableFuture.runAsync(() -> {
                        R r = productFeignService.skuInfo(relation.getSkuId());
                        Assert.isTrue(r.getCode() == 0, BizCodeEnum.FEIGN_SERVICE_EXCEPTION);
                        SkuInfoDTO skuInfoDTO = r.getData("skuInfo", new TypeReference<SkuInfoDTO>() {
                        });
                        seckillSkuRedisDTO.setSkuInfoDTO(skuInfoDTO);
                    }, executor);


                    // 2.查询销售属性的组合信息
                    CompletableFuture<Void> saleAttrFuture = CompletableFuture.runAsync(() -> {
                        R r = productFeignService.getSaleAttrNameAndValues(relation.getSkuId());
                        if (r.getCode() == 0) {
                            List<String> data = r.getData(new TypeReference<List<String>>() {
                            });
                            seckillSkuRedisDTO.setSkuAttr(data);
                        }
                    }, executor);

                    try {
                        CompletableFuture.allOf(skuInfoFuture, saleAttrFuture).get();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }

                    // sku的秒杀数据
                    seckillSkuRedisDTO.setSeckillSkuRelationDTO(relation);

                    // 商品开始秒杀的时间、结束秒杀的时间
                    seckillSkuRedisDTO.setStartTime(item.getStartTime().getTime());
                    seckillSkuRedisDTO.setEndTime(item.getEndTime().getTime());

                    // 秒杀请求需要用的随机码
                    String randomCode = UUID.randomUUID().toString().replace("-", "");
                    seckillSkuRedisDTO.setRandomCode(randomCode);

                    String jsonStr = JSON.toJSONString(seckillSkuRedisDTO);
                    skusOperations.put(skuRedisKey, jsonStr);

                    // TODO 设置秒杀商品的过期时间，不知道怎么加
                    long timeOut = item.getEndTime().getTime() - new Date().getTime();
//                    stringRedisTemplate.expire(skuRedisKey, timeOut, TimeUnit.MILLISECONDS);

                    // 为了限流：使用秒杀商品的库存作为分布式的信号量
                    RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SECKILL_SKU_STOCK_SEMAPHORE
                            + randomCode);
                    semaphore.trySetPermits(relation.getSeckillCount());
                    // 设置信号量的过期时间
                    stringRedisTemplate.expire(SeckillConstant.SECKILL_SKU_STOCK_SEMAPHORE
                            + randomCode, timeOut, TimeUnit.MILLISECONDS);
                }

            });

        });
    }

    /**
     * 缓存秒杀活动场次以及对应的skuId
     *
     * @param seckillSessionDTOs
     */
    private void cacheSeckillSession(List<SeckillSessionDTO> seckillSessionDTOs) {
        if (CollectionUtils.isNotEmpty(seckillSessionDTOs)) {
            seckillSessionDTOs.stream().forEach(item -> {
                StringBuilder redisKey = new StringBuilder(SeckillConstant.SESSIONS_CACHE_PREFIX);
                redisKey.append(item.getStartTime().getTime()).append("_").append(item.getEndTime().getTime());

                if (!stringRedisTemplate.hasKey(redisKey.toString())) {
                    List<String> skuIds = item.getRelations().stream().map(relation -> relation.getPromotionSessionId()
                            + "_" + relation.getSkuId().toString()).collect(Collectors.toList());
                    stringRedisTemplate.opsForList().leftPushAll(redisKey.toString(), skuIds);
                    // 设置秒杀场次的过期时间
                    long timeOut = item.getEndTime().getTime() - new Date().getTime();
                    stringRedisTemplate.expire(redisKey.toString(), timeOut, TimeUnit.MILLISECONDS);
                }
            });
        }
    }
}
